package cn.ckx.coding.common.utils.redis;

import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 利用redis的increment获取全局自增序列，每天重置初始值
 * 每天容纳1亿个自增序列
 * 返回一个非负长整型数，低27位保存自增序列，高位保存yyMMdd日期值，高位不足补0
 *
 * @author kunxin.cai
 */
@Slf4j
public class SerialNumber {

    private static final String CHECK_AND_RESET =
            "local var = redis.call('get', KEYS[1]) \n" +
            "if var and var >= ARGV[1] \n" +
                    "then \n" +
                        "return 0\n" +
                    "else \n" +
                        "redis.call('set', KEYS[1], ARGV[1]) \n" +
                        "redis.call('expire', KEYS[1], " + TimeUnit.DAYS.toSeconds(1)+") \n" +
                        "return 1 \n" +
                    "end";

    /**
     * 取非负长整型数的低27位保存0 ~ 99999999的1亿个自增序列
     */
    private static final int LOW_BITS = 27;

    /**
     * 取非负长整型数的低27位保存0 ~ 99999999的1亿个自增序列
     */
    private static final long LOW_BIT_MAX_VALUE = 99999999L;

    /**
     * 自增序列业务KEY，redis中用于每日自增序列key前缀，用作业务区分
     */
    private final String key;

    private final StringRedisTemplate redisTemplate;

    public static Builder builder() {
        return new Builder();
    }

    private SerialNumber(Builder builder) {
        this.key = builder.key;
        this.redisTemplate = builder.redisTemplate;
    }

    /**
     * 利用redis的increment获取全局自增序列，业务KEY加上当前日期后缀，使得每天从redis获取的自增值重新从1开始
     * @return long 下一个序参数
     */
    public long next() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMdd");
        String currentDate = dateFormat.format(new Date());
        String actualKey = String.format("%s:%s", key, currentDate);

        long increment = Objects.requireNonNull(redisTemplate.opsForValue().increment(actualKey)) - 1L;
        if (increment == 0L) {
            redisTemplate.expire(actualKey, 1, TimeUnit.DAYS);
        }

        if (increment > LOW_BIT_MAX_VALUE) {
            throw new RuntimeException(String.format("low bit autoincrement [%d] exceed max value for [%s]", increment, currentDate));
        }

        return Long.valueOf(currentDate) << LOW_BITS | increment;
    }

    public boolean reset(long serialNumber) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMdd");
        String currentDate = dateFormat.format(new Date());
        String actualKey = String.format("%s:%s", key, currentDate);

        String date = String.valueOf(serialNumber >> LOW_BITS);
        if (!Objects.equals(currentDate, date)) {
            return false;
        }

        long expectedValue = Long.MAX_VALUE >> (64 - LOW_BITS) & serialNumber + 1L;
        List<String> keys = Collections.singletonList(actualKey);
        Long result = redisTemplate.execute(new DefaultRedisScript<>(CHECK_AND_RESET, Long.class), keys, String.valueOf(expectedValue));
        if (Objects.requireNonNull(result) == 1L) {
            log.info("key [{}] serialNumber [{}] reset", actualKey, serialNumber);
        }

        return result == 1L;
    }

    @Data
    @Accessors(chain = true)
    public static class Builder {

        private String key;

        private StringRedisTemplate redisTemplate;

        public SerialNumber build() {
            return new SerialNumber(this);
        }

    }
}
